#!@bash@/bin/bash

# Prepare to use tools from Nixpkgs.
PATH=@DEP_PATH@${PATH:+:}$PATH

set -euo pipefail

export TEXTDOMAIN=home-manager
export TEXTDOMAINDIR=@OUT@/share/locale

# shellcheck disable=1091
source @HOME_MANAGER_LIB@

function removeByName() {
  nix profile list \
      | { grep "$1" || test $? = 1; } \
      | cut -d ' ' -f 4 \
      | xargs -t $DRY_RUN_CMD nix profile remove $VERBOSE_ARG
}

function setNixProfileCommands() {
    if [[ -e ~/.nix-profile/manifest.json ]] ; then
        LIST_OUTPATH_CMD="nix profile list"
        REMOVE_CMD="removeByName"
    else
        LIST_OUTPATH_CMD="nix-env -q --out-path"
        REMOVE_CMD="nix-env -q"
    fi
}

function setVerboseAndDryRun() {
    if [[ -v VERBOSE ]]; then
        export VERBOSE_ARG="--verbose"
    else
        export VERBOSE_ARG=""
    fi

    if [[ -v DRY_RUN ]] ; then
        export DRY_RUN_CMD=echo
    else
        export DRY_RUN_CMD=""
    fi
}

function setWorkDir() {
    if [[ ! -v WORK_DIR ]]; then
        WORK_DIR="$(mktemp --tmpdir -d home-manager-build.XXXXXXXXXX)"
        # shellcheck disable=2064
        trap "rm -r '$WORK_DIR'" EXIT
    fi
}

# Attempts to set the HOME_MANAGER_CONFIG global variable.
#
# If no configuration file can be found then this function will print
# an error message and exit with an error code.
function setConfigFile() {
    if [[ -v HOME_MANAGER_CONFIG ]] ; then
        if [[ ! -e "$HOME_MANAGER_CONFIG" ]] ; then
            _i "No configuration file found at %s" \
               "$HOME_MANAGER_CONFIG" >&2
            exit 1
        fi

        HOME_MANAGER_CONFIG="$(realpath "$HOME_MANAGER_CONFIG")"
        return
    fi

    local defaultConfFile="${XDG_CONFIG_HOME:-$HOME/.config}/nixpkgs/home.nix"
    local confFile
    for confFile in "$defaultConfFile" \
                    "$HOME/.nixpkgs/home.nix" ; do
        if [[ -e "$confFile" ]] ; then
            HOME_MANAGER_CONFIG="$(realpath "$confFile")"
            return
        fi
    done

    _i "No configuration file found. Please create one at %s" \
       "$defaultConfFile" >&2
    exit 1
}

function setHomeManagerNixPath() {
    local path
    for path in "@HOME_MANAGER_PATH@" \
                "${XDG_CONFIG_HOME:-$HOME/.config}/nixpkgs/home-manager" \
                "$HOME/.nixpkgs/home-manager" ; do
        if [[ -e "$path" || "$path" =~ ^https?:// ]] ; then
            export NIX_PATH="home-manager=$path${NIX_PATH:+:}$NIX_PATH"
            return
        fi
    done
}

function setFlakeAttribute() {
    local configFlake="${XDG_CONFIG_HOME:-$HOME/.config}/nixpkgs/flake.nix"
    if [[ -z $FLAKE_ARG && ! -v HOME_MANAGER_CONFIG && -e "$configFlake" ]]; then
        FLAKE_ARG="$(dirname "$(readlink -f "$configFlake")")"
    fi

    if [[ -n "$FLAKE_ARG" ]]; then
        local flake="${FLAKE_ARG%#*}"
        case $FLAKE_ARG in
            *#*)
                local name="${FLAKE_ARG#*#}"
            ;;
            *)
                local name="$USER@$(hostname)"
                if [ "$(nix eval "$flake#homeConfigurations" --apply "x: x ? \"$name\"")" = "false" ]; then
                    name="$USER"
                fi
            ;;
        esac
        export FLAKE_CONFIG_URI="$flake#homeConfigurations.\"$name\""
    fi
}

function doInspectOption() {
    setFlakeAttribute
    if [[ -v FLAKE_CONFIG_URI ]]; then
        _iError "Can't inspect options of a flake configuration"
        exit 1
    fi
    setConfigFile
    setHomeManagerNixPath

    local extraArgs=("$@")

    for p in "${EXTRA_NIX_PATH[@]}"; do
        extraArgs=("${extraArgs[@]}" "-I" "$p")
    done

    if [[ -v VERBOSE ]]; then
        extraArgs=("${extraArgs[@]}" "--show-trace")
    fi

    local HOME_MANAGER_CONFIG_NIX HOME_MANAGER_CONFIG_ATTRIBUTE_NIX
    HOME_MANAGER_CONFIG_NIX=${HOME_MANAGER_CONFIG//'\'/'\\'}
    HOME_MANAGER_CONFIG_NIX=${HOME_MANAGER_CONFIG_NIX//'"'/'\"'}
    HOME_MANAGER_CONFIG_NIX=${HOME_MANAGER_CONFIG_NIX//$'\n'/$'\\n'}
    HOME_MANAGER_CONFIG_ATTRIBUTE_NIX=${HOME_MANAGER_CONFIG_ATTRIBUTE//'\'/'\\'}
    HOME_MANAGER_CONFIG_ATTRIBUTE_NIX=${HOME_MANAGER_CONFIG_ATTRIBUTE_NIX//'"'/'\"'}
    HOME_MANAGER_CONFIG_ATTRIBUTE_NIX=${HOME_MANAGER_CONFIG_ATTRIBUTE_NIX//$'\n'/$'\\n'}
    local modulesExpr
    modulesExpr="let confPath = \"${HOME_MANAGER_CONFIG_NIX}\"; "
    modulesExpr+="confAttr = \"${HOME_MANAGER_CONFIG_ATTRIBUTE_NIX}\"; in "
    modulesExpr+="(import <home-manager/modules> {"
    modulesExpr+=" configuration = if confAttr == \"\" then confPath else (import confPath).\${confAttr};"
    modulesExpr+=" pkgs = import <nixpkgs> {}; check = true; })"

    nixos-option \
        --options_expr "$modulesExpr.options" \
        --config_expr "$modulesExpr.config" \
        "${extraArgs[@]}" \
        "${PASSTHROUGH_OPTS[@]}"
}

function doInstantiate() {
    setFlakeAttribute
    if [[ -v FLAKE_CONFIG_URI ]]; then
        _i "Can't instantiate a flake configuration" >&2
        exit 1
    fi
    setConfigFile
    setHomeManagerNixPath

    local extraArgs=()

    for p in "${EXTRA_NIX_PATH[@]}"; do
        extraArgs=("${extraArgs[@]}" "-I" "$p")
    done

    if [[ -v VERBOSE ]]; then
        extraArgs=("${extraArgs[@]}" "--show-trace")
    fi

    nix-instantiate \
        "<home-manager/home-manager/home-manager.nix>" \
        "${extraArgs[@]}" \
        "${PASSTHROUGH_OPTS[@]}" \
        --argstr confPath "$HOME_MANAGER_CONFIG" \
        --argstr confAttr "$HOME_MANAGER_CONFIG_ATTRIBUTE"
}

function doBuildAttr() {
    setConfigFile
    setHomeManagerNixPath

    local extraArgs=("$@")

    for p in "${EXTRA_NIX_PATH[@]}"; do
        extraArgs=("${extraArgs[@]}" "-I" "$p")
    done

    if [[ -v VERBOSE ]]; then
        extraArgs=("${extraArgs[@]}" "--show-trace")
    fi

    nix-build \
        "<home-manager/home-manager/home-manager.nix>" \
        "${extraArgs[@]}" \
        "${PASSTHROUGH_OPTS[@]}" \
        --argstr confPath "$HOME_MANAGER_CONFIG" \
        --argstr confAttr "$HOME_MANAGER_CONFIG_ATTRIBUTE"
}

function doBuildFlake() {
    local extraArgs=("$@")

    if [[ -v VERBOSE ]]; then
        extraArgs=("${extraArgs[@]}" "--verbose")
    fi

    nix build \
        "${extraArgs[@]}" \
        "${PASSTHROUGH_OPTS[@]}"
}

# Presents news to the user. Takes as argument the path to a "news
# info" file as generated by `buildNews`.
function presentNews() {
    local infoFile="$1"

    # shellcheck source=/dev/null
    . "$infoFile"

    # shellcheck disable=2154
    if [[ $newsNumUnread -eq 0 ]]; then
        return
    elif [[ "$newsDisplay" == "silent" ]]; then
        return
    elif [[ "$newsDisplay" == "notify" ]]; then
        local cmd msg
        cmd="$(basename "$0")"
        msg="$(_ip \
                $'There is %d unread and relevant news item.\nRead it by running the command "%s news".' \
                $'There are %d unread and relevant news items.\nRead them by running the command "%s news".' \
                "$newsNumUnread" "$newsNumUnread" "$cmd")"

        # Not actually an error but here stdout is reserved for
        # nix-build output.
        echo $'\n'"$msg"$'\n' >&2

        if [[ -v DISPLAY ]] && type -P notify-send > /dev/null; then
            notify-send "Home Manager" "$msg"
        fi
    elif [[ "$newsDisplay" == "show" ]]; then
        doShowNews --unread
    else
        _i 'Unknown "news.display" setting "%s".' "$newsDisplay" >&2
    fi
}

function doEdit() {
    if [[ ! -v EDITOR || -z $EDITOR ]]; then
        # shellcheck disable=2016
        _i 'Please set the $EDITOR environment variable' >&2
        return 1
    fi

    setConfigFile

    # Don't quote $EDITOR in order to support values including options, e.g.,
    # "code --wait".
    #
    # shellcheck disable=2086
    exec $EDITOR "$HOME_MANAGER_CONFIG"
}

function doBuild() {
    if [[ ! -w . ]]; then
        _i 'Cannot run build in read-only directory' >&2
        return 1
    fi

    setWorkDir

    setFlakeAttribute
    if [[ -v FLAKE_CONFIG_URI ]]; then
        doBuildFlake \
            "$FLAKE_CONFIG_URI.activationPackage" \
            ${DRY_RUN+--dry-run} \
            ${NO_OUT_LINK+--no-link} \
            || return
    else
        doBuildAttr \
            ${NO_OUT_LINK+--no-out-link} \
            --attr activationPackage \
        || return

        local newsInfo
        newsInfo=$(buildNews)

        presentNews "$newsInfo"
    fi
}

function doSwitch() {
    setWorkDir

    local generation

    # Build the generation and run the activate script. Note, we
    # specify an output link so that it is treated as a GC root. This
    # prevents an unfortunately timed GC from removing the generation
    # before activation completes.
    generation="$WORK_DIR/generation"

    setFlakeAttribute
    if [[ -v FLAKE_CONFIG_URI ]]; then
        doBuildFlake \
            "$FLAKE_CONFIG_URI.activationPackage" \
            --out-link "$generation" \
            && "$generation/activate" || return
    else
        doBuildAttr \
            --out-link "$generation" \
            --attr activationPackage \
            && "$generation/activate" || return

        local newsInfo
        newsInfo=$(buildNews)

        presentNews "$newsInfo"
    fi
}

function doListGens() {
    # Whether to colorize the generations output.
    local color="never"
    if [[ ! -v NO_COLOR && -t 1 ]]; then
        color="always"
    fi

    pushd "$NIX_STATE_DIR/profiles/per-user/$USER" > /dev/null
    # shellcheck disable=2012
    ls --color=$color -gG --time-style=long-iso --sort time home-manager-*-link \
        | cut -d' ' -f 4- \
        | sed -E 's/home-manager-([[:digit:]]*)-link/: id \1/'
    popd > /dev/null
}

# Removes linked generations. Takes as arguments identifiers of
# generations to remove.
function doRmGenerations() {
    setVerboseAndDryRun

    pushd "$NIX_STATE_DIR/profiles/per-user/$USER" > /dev/null

    for generationId in "$@"; do
        local linkName="home-manager-$generationId-link"

        if [[ ! -e $linkName ]]; then
            _i 'No generation with ID %s' "$generationId" >&2
        elif [[ $linkName == $(readlink home-manager) ]]; then
            _i 'Cannot remove the current generation %s' "$generationId" >&2
        else
            _i 'Removing generation %s' "$generationId"
            $DRY_RUN_CMD rm $VERBOSE_ARG $linkName
        fi
    done

    popd > /dev/null
}

function doRmAllGenerations() {
    $DRY_RUN_CMD rm $VERBOSE_ARG \
        "$NIX_STATE_DIR/profiles/per-user/$USER/home-manager"*
}

function doExpireGenerations() {
    local profileDir="$NIX_STATE_DIR/profiles/per-user/$USER"

    local generations
    generations="$( \
        find "$profileDir" -name 'home-manager-*-link' -not -newermt "$1" \
        | sed 's/^.*-\([0-9]*\)-link$/\1/' \
    )"

    if [[ -n $generations ]]; then
        # shellcheck disable=2086
        doRmGenerations $generations
    elif [[ -v VERBOSE ]]; then
        _i "No generations to expire"
    fi
}

function doListPackages() {
    setNixProfileCommands
    local outPath
    outPath="$($LIST_OUTPATH_CMD | grep -o '/.*home-manager-path$')"
    if [[ -n "$outPath" ]] ; then
        nix-store -q --references "$outPath" | sed 's/[^-]*-//'
    else
        _i 'No home-manager packages seem to be installed.' >&2
    fi
}

function newsReadIdsFile() {
    local dataDir="${XDG_DATA_HOME:-$HOME/.local/share}/home-manager"
    local path="$dataDir/news-read-ids"

    # If the path doesn't exist then we should create it, otherwise
    # Nix will error out when we attempt to use builtins.readFile.
    if [[ ! -f "$path" ]]; then
        mkdir -p "$dataDir"
        touch "$path"
    fi

    echo "$path"
}

# Builds news meta information to be sourced into this script.
#
# Note, we suppress build output to remove unnecessary verbosity. We
# put the output in the work directory to avoid the risk of an
# unfortunately timed GC removing it.
function buildNews() {
    local output
    output="$WORK_DIR/news-info.sh"

    doBuildAttr \
        --out-link "$output" \
        --no-build-output \
        --quiet \
        --arg check false \
        --argstr newsReadIdsFile "$(newsReadIdsFile)" \
        --attr newsInfo \
        > /dev/null

    echo "$output"
}

function doShowNews() {
    setWorkDir

    local infoFile
    infoFile=$(buildNews) || return 1

    # shellcheck source=/dev/null
    . "$infoFile"

    # shellcheck disable=2154
    case $1 in
        --all)
            ${PAGER:-less} "$newsFileAll"
            ;;
        --unread)
            ${PAGER:-less} "$newsFileUnread"
            ;;
        *)
            _i 'Unknown argument %s' "$1"
            return 1
    esac

    # shellcheck disable=2154
    if [[ -s "$newsUnreadIdsFile" ]]; then
        local newsReadIdsFile
        newsReadIdsFile="$(newsReadIdsFile)"
        cat "$newsUnreadIdsFile" >> "$newsReadIdsFile"
    fi
}

function doUninstall() {
    setVerboseAndDryRun
    setNixProfileCommands

    _i 'This will remove Home Manager from your system.'

    if [[ -v DRY_RUN ]]; then
        _i 'This is a dry run, nothing will actually be uninstalled.'
    fi

    local confirmation
    read -r -n 1 -p "$(_i 'Really uninstall Home Manager?') [y/n] " confirmation
    echo

    case $confirmation in
        y|Y)
            _i "Switching to empty Home Manager configuration..."
            HOME_MANAGER_CONFIG="$(mktemp --tmpdir home-manager.XXXXXXXXXX)"
            echo "{ lib, ... }: { home.file = lib.mkForce {}; }" > "$HOME_MANAGER_CONFIG"
            doSwitch
            $DRY_RUN_CMD $REMOVE_CMD home-manager-path || true
            rm "$HOME_MANAGER_CONFIG"
            $DRY_RUN_CMD rm $VERBOSE_ARG -r \
                "${XDG_DATA_HOME:-$HOME/.local/share}/home-manager"
            $DRY_RUN_CMD rm $VERBOSE_ARG \
                "$NIX_STATE_DIR/gcroots/per-user/$USER/current-home"
            ;;
        *)
            _i "Yay!"
            exit 0
            ;;
    esac

    local deleteProfiles
    read -r -n 1 \
         -p "$(_i 'Remove all Home Manager generations?') [y/n] " \
         deleteProfiles
    echo

    case $deleteProfiles in
        y|Y)
            doRmAllGenerations
            _i 'All generations are now eligible for garbage collection.'
            ;;
        *)
            _i 'Leaving generations but they may still be garbage collected.'
            ;;
    esac

    _i "Home Manager is uninstalled but your home.nix is left untouched."
}

function doHelp() {
    echo "Usage: $0 [OPTION] COMMAND"
    echo
    echo "Options"
    echo
    echo "  -f FILE           The home configuration file."
    echo "                    Default is '~/.config/nixpkgs/home.nix'."
    echo "  -A ATTRIBUTE      Optional attribute that selects a configuration"
    echo "                    expression in the configuration file."
    echo "  -I PATH           Add a path to the Nix expression search path."
    echo "  --flake flake-uri Use Home Manager configuration at flake-uri"
    echo "  -b EXT            Move existing files to new path rather than fail."
    echo "  -v                Verbose output"
    echo "  -n                Do a dry run, only prints what actions would be taken"
    echo "  -h                Print this help"
    echo "  --version         Print the Home Manager version"
    echo
    echo "Options passed on to nix-build(1)"
    echo
    echo "  --arg(str) NAME VALUE    Override inputs passed to home-manager.nix"
    echo "  --cores NUM"
    echo "  --debug"
    echo "  --impure"
    echo "  --keep-failed"
    echo "  --keep-going"
    echo "  -j, --max-jobs NUM"
    echo "  --option NAME VALUE"
    echo "  --show-trace"
    echo "  --(no-)substitute"
    echo "  --no-out-link            Do not create a symlink to the output path"
    echo "  --no-write-lock-file"
    echo "  --builders VALUE"
    echo
    echo "Commands"
    echo
    echo "  help         Print this help"
    echo
    echo "  edit         Open the home configuration in \$EDITOR"
    echo
    echo "  option OPTION.NAME"
    echo "               Inspect configuration option named OPTION.NAME."
    echo
    echo "  build        Build configuration into result directory"
    echo
    echo "  instantiate  Instantiate the configuration and print the resulting derivation"
    echo
    echo "  switch       Build and activate configuration"
    echo
    echo "  generations  List all home environment generations"
    echo
    echo "  remove-generations ID..."
    echo "      Remove indicated generations. Use 'generations' command to"
    echo "      find suitable generation numbers."
    echo
    echo "  expire-generations TIMESTAMP"
    echo "      Remove generations older than TIMESTAMP where TIMESTAMP is"
    echo "      interpreted as in the -d argument of the date tool. For"
    echo "      example \"-30 days\" or \"2018-01-01\"."
    echo
    echo "  packages     List all packages installed in home-manager-path"
    echo
    echo "  news         Show news entries in a pager"
    echo
    echo "  uninstall    Remove Home Manager"
}

readonly NIX_STATE_DIR="${NIX_STATE_DIR:-/nix/var/nix}"

EXTRA_NIX_PATH=()
HOME_MANAGER_CONFIG_ATTRIBUTE=""
PASSTHROUGH_OPTS=()
COMMAND=""
COMMAND_ARGS=()
FLAKE_ARG=""

while [[ $# -gt 0 ]]; do
    opt="$1"
    shift
    case $opt in
        build|instantiate|option|edit|expire-generations|generations|help|news|packages|remove-generations|switch|uninstall)
            COMMAND="$opt"
            ;;
        -A)
            HOME_MANAGER_CONFIG_ATTRIBUTE="$1"
            shift
            ;;
        -I)
            EXTRA_NIX_PATH+=("$1")
            shift
            ;;
        -b)
            export HOME_MANAGER_BACKUP_EXT="$1"
            shift
            ;;
        -f|--file)
            HOME_MANAGER_CONFIG="$1"
            shift
            ;;
        --flake)
            FLAKE_ARG="$1"
            shift
            ;;
        --recreate-lock-file|--no-update-lock-file|--no-write-lock-file|--no-registries|--commit-lock-file)
            PASSTHROUGH_OPTS+=("$opt")
            ;;
        --update-input)
            PASSTHROUGH_OPTS+=("$opt" "$1")
            shift
            ;;
        --override-input)
            PASSTHROUGH_OPTS+=("$opt" "$1" "$2")
            shift 2
            ;;
        --experimental-features)
            PASSTHROUGH_OPTS+=("$opt" "$1")
            shift
            ;;
        --extra-experimental-features)
            PASSTHROUGH_OPTS+=("$opt" "$1")
            shift
            ;;
        --no-out-link)
            NO_OUT_LINK=1
            ;;
        -h|--help)
            doHelp
            exit 0
            ;;
        -n|--dry-run)
            export DRY_RUN=1
            ;;
        --option|--arg|--argstr)
            PASSTHROUGH_OPTS+=("$opt" "$1" "$2")
            shift 2
            ;;
        -j|--max-jobs|--cores|--builders)
            PASSTHROUGH_OPTS+=("$opt" "$1")
            shift
            ;;
        --debug|--keep-failed|--keep-going|--show-trace\
            |--substitute|--no-substitute|--impure)
            PASSTHROUGH_OPTS+=("$opt")
            ;;
        -v|--verbose)
            export VERBOSE=1
            ;;
        --version)
            echo 22.05
            exit 0
            ;;
        *)
            case $COMMAND in
                expire-generations|remove-generations|option)
                    COMMAND_ARGS+=("$opt")
                    ;;
                *)
                    _iError "%s: unknown option '%s'" "$0" "$opt" >&2
                    _i "Run '%s --help' for usage help" "$0" >&2
                    exit 1
                    ;;
            esac
            ;;
    esac
done

if [[ -z $COMMAND ]]; then
    doHelp >&2
    exit 1
fi

case $COMMAND in
    edit)
        doEdit
        ;;
    build)
        doBuild
        ;;
    instantiate)
        doInstantiate
        ;;
    switch)
        doSwitch
        ;;
    generations)
        doListGens
        ;;
    remove-generations)
        doRmGenerations "${COMMAND_ARGS[@]}"
        ;;
    expire-generations)
        if [[ ${#COMMAND_ARGS[@]} != 1 ]]; then
            _i 'expire-generations expects one argument, got %d.' "${#COMMAND_ARGS[@]}" >&2
            exit 1
        else
            doExpireGenerations "${COMMAND_ARGS[@]}"
        fi
        ;;
    option)
        doInspectOption "${COMMAND_ARGS[@]}"
        ;;
    packages)
        doListPackages
        ;;
    news)
        doShowNews --all
        ;;
    uninstall)
        doUninstall
        ;;
    help)
        doHelp
        ;;
    *)
        _iError 'Unknown command: %s' "$COMMAND" >&2
        doHelp >&2
        exit 1
        ;;
esac

# vim: ft=bash
